// ========================================================================
// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.StringTokenizer;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/*--------------------------------------------------------------*/
/**
 * XML Parser wrapper. This class wraps any standard JAXP1.1 parser with convieniant error and entity handlers and a mini dom-like document tree.
 * <P>
 * By default, the parser is created as a validating parser only if xerces is present. This can be configured by setting the "org.eclipse.jetty.xml.XmlParser.Validating" system property.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class XmlParser
{

	private static final Logger LOG = Log.getLogger(XmlParser.class);

	private Map<String, URL> _redirectMap = new HashMap<String, URL>();
	private SAXParser _parser;
	private Map<String, ContentHandler> _observerMap;
	private Stack<ContentHandler> _observers = new Stack<ContentHandler>();
	private String _xpath;
	private Object _xpaths;
	private String _dtd;

	/* ------------------------------------------------------------ */
	/**
	 * Construct
	 */
	public XmlParser()
	{
		SAXParserFactory factory = SAXParserFactory.newInstance();
		boolean validating_dft = factory.getClass().toString().startsWith("org.apache.xerces.");
		String validating_prop = System.getProperty("org.eclipse.jetty.xml.XmlParser.Validating", validating_dft ? "true" : "false");
		boolean validating = Boolean.valueOf(validating_prop).booleanValue();
		setValidating(validating);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Constructor.
	 */
	public XmlParser(boolean validating)
	{
		setValidating(validating);
	}

	/* ------------------------------------------------------------ */
	public void setValidating(boolean validating)
	{
		try
		{
			SAXParserFactory factory = SAXParserFactory.newInstance();
			factory.setValidating(validating);
			_parser = factory.newSAXParser();

			try
			{
				if (validating)
					_parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/schema", validating);
			} catch (Exception e)
			{
				if (validating)
					LOG.warn("Schema validation may not be supported: ", e);
				else
					LOG.ignore(e);
			}

			_parser.getXMLReader().setFeature("http://xml.org/sax/features/validation", validating);
			_parser.getXMLReader().setFeature("http://xml.org/sax/features/namespaces", true);
			_parser.getXMLReader().setFeature("http://xml.org/sax/features/namespace-prefixes", false);
			try
			{
				if (validating)
					_parser.getXMLReader().setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", validating);
			} catch (Exception e)
			{
				LOG.warn(e.getMessage());
			}
		} catch (Exception e)
		{
			LOG.warn(Log.EXCEPTION, e);
			throw new Error(e.toString());
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param name
	 * @param entity
	 */
	public synchronized void redirectEntity(String name, URL entity)
	{
		if (entity != null)
			_redirectMap.put(name, entity);
	}

	/* ------------------------------------------------------------ */
	/**
	 * @return Returns the xpath.
	 */
	public String getXpath()
	{
		return _xpath;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Set an XPath A very simple subset of xpath is supported to select a partial tree. Currently only path like "/node1/nodeA | /node1/nodeB" are supported.
	 * 
	 * @param xpath The xpath to set.
	 */
	public void setXpath(String xpath)
	{
		_xpath = xpath;
		StringTokenizer tok = new StringTokenizer(xpath, "| ");
		while (tok.hasMoreTokens())
			_xpaths = LazyList.add(_xpaths, tok.nextToken());
	}

	/* ------------------------------------------------------------ */
	public String getDTD()
	{
		return _dtd;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Add a ContentHandler. Add an additional _content handler that is triggered on a tag name. SAX events are passed to the ContentHandler provided from a matching start element to the corresponding end element. Only a single _content handler can be registered against each tag.
	 * 
	 * @param trigger Tag local or q name.
	 * @param observer SAX ContentHandler
	 */
	public synchronized void addContentHandler(String trigger, ContentHandler observer)
	{
		if (_observerMap == null)
			_observerMap = new HashMap();
		_observerMap.put(trigger, observer);
	}

	/* ------------------------------------------------------------ */
	public synchronized Node parse(InputSource source) throws IOException, SAXException
	{
		_dtd = null;
		Handler handler = new Handler();
		XMLReader reader = _parser.getXMLReader();
		reader.setContentHandler(handler);
		reader.setErrorHandler(handler);
		reader.setEntityResolver(handler);
		if (LOG.isDebugEnabled())
			LOG.debug("parsing: sid=" + source.getSystemId() + ",pid=" + source.getPublicId());
		_parser.parse(source, handler);
		if (handler._error != null)
			throw handler._error;
		Node doc = (Node)handler._top.get(0);
		handler.clear();
		return doc;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Parse String URL.
	 */
	public synchronized Node parse(String url) throws IOException, SAXException
	{
		if (LOG.isDebugEnabled())
			LOG.debug("parse: " + url);
		return parse(new InputSource(url));
	}

	/* ------------------------------------------------------------ */
	/**
	 * Parse File.
	 */
	public synchronized Node parse(File file) throws IOException, SAXException
	{
		if (LOG.isDebugEnabled())
			LOG.debug("parse: " + file);
		return parse(new InputSource(Resource.toURL(file).toString()));
	}

	/* ------------------------------------------------------------ */
	/**
	 * Parse InputStream.
	 */
	public synchronized Node parse(InputStream in) throws IOException, SAXException
	{
		_dtd = null;
		Handler handler = new Handler();
		XMLReader reader = _parser.getXMLReader();
		reader.setContentHandler(handler);
		reader.setErrorHandler(handler);
		reader.setEntityResolver(handler);
		_parser.parse(new InputSource(in), handler);
		if (handler._error != null)
			throw handler._error;
		Node doc = (Node)handler._top.get(0);
		handler.clear();
		return doc;
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	private class NoopHandler extends DefaultHandler
	{

		Handler _next;
		int _depth;

		NoopHandler(Handler next)
		{
			this._next = next;
		}

		/* ------------------------------------------------------------ */
		@Override
		public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException
		{
			_depth++;
		}

		/* ------------------------------------------------------------ */
		@Override
		public void endElement(String uri, String localName, String qName) throws SAXException
		{
			if (_depth == 0)
				_parser.getXMLReader().setContentHandler(_next);
			else
				_depth--;
		}
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	private class Handler extends DefaultHandler
	{

		Node _top = new Node(null, null, null);
		SAXParseException _error;
		private Node _context = _top;
		private NoopHandler _noop;

		Handler()
		{
			_noop = new NoopHandler(this);
		}

		/* ------------------------------------------------------------ */
		void clear()
		{
			_top = null;
			_error = null;
			_context = null;
		}

		/* ------------------------------------------------------------ */
		@Override
		public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException
		{
			String name = null;
			if (_parser.isNamespaceAware())
				name = localName;

			if (name == null || "".equals(name))
				name = qName;

			Node node = new Node(_context, name, attrs);

			// check if the node matches any xpaths set?
			if (_xpaths != null)
			{
				String path = node.getPath();
				boolean match = false;
				for (int i = LazyList.size(_xpaths); !match && i-- > 0;)
				{
					String xpath = (String)LazyList.get(_xpaths, i);

					match = path.equals(xpath) || xpath.startsWith(path) && xpath.length() > path.length() && xpath.charAt(path.length()) == '/';
				}

				if (match)
				{
					_context.add(node);
					_context = node;
				}
				else
				{
					_parser.getXMLReader().setContentHandler(_noop);
				}
			}
			else
			{
				_context.add(node);
				_context = node;
			}

			ContentHandler observer = null;
			if (_observerMap != null)
				observer = (ContentHandler)_observerMap.get(name);
			_observers.push(observer);

			for (int i = 0; i < _observers.size(); i++)
				if (_observers.get(i) != null)
					((ContentHandler)_observers.get(i)).startElement(uri, localName, qName, attrs);
		}

		/* ------------------------------------------------------------ */
		@Override
		public void endElement(String uri, String localName, String qName) throws SAXException
		{
			_context = _context._parent;
			for (int i = 0; i < _observers.size(); i++)
				if (_observers.get(i) != null)
					((ContentHandler)_observers.get(i)).endElement(uri, localName, qName);
			_observers.pop();
		}

		/* ------------------------------------------------------------ */
		@Override
		public void ignorableWhitespace(char buf[], int offset, int len) throws SAXException
		{
			for (int i = 0; i < _observers.size(); i++)
				if (_observers.get(i) != null)
					((ContentHandler)_observers.get(i)).ignorableWhitespace(buf, offset, len);
		}

		/* ------------------------------------------------------------ */
		@Override
		public void characters(char buf[], int offset, int len) throws SAXException
		{
			_context.add(new String(buf, offset, len));
			for (int i = 0; i < _observers.size(); i++)
				if (_observers.get(i) != null)
					((ContentHandler)_observers.get(i)).characters(buf, offset, len);
		}

		/* ------------------------------------------------------------ */
		@Override
		public void warning(SAXParseException ex)
		{
			LOG.debug(Log.EXCEPTION, ex);
			LOG.warn("WARNING@" + getLocationString(ex) + " : " + ex.toString());
		}

		/* ------------------------------------------------------------ */
		@Override
		public void error(SAXParseException ex) throws SAXException
		{
			// Save error and continue to report other errors
			if (_error == null)
				_error = ex;
			LOG.debug(Log.EXCEPTION, ex);
			LOG.warn("ERROR@" + getLocationString(ex) + " : " + ex.toString());
		}

		/* ------------------------------------------------------------ */
		@Override
		public void fatalError(SAXParseException ex) throws SAXException
		{
			_error = ex;
			LOG.debug(Log.EXCEPTION, ex);
			LOG.warn("FATAL@" + getLocationString(ex) + " : " + ex.toString());
			throw ex;
		}

		/* ------------------------------------------------------------ */
		private String getLocationString(SAXParseException ex)
		{
			return ex.getSystemId() + " line:" + ex.getLineNumber() + " col:" + ex.getColumnNumber();
		}

		/* ------------------------------------------------------------ */
		@SuppressWarnings("null")
		@Override
		public InputSource resolveEntity(String pid, String sid)
		{
			if (LOG.isDebugEnabled())
				LOG.debug("resolveEntity(" + pid + ", " + sid + ")");

			if (sid != null && sid.endsWith(".dtd"))
				_dtd = sid;

			URL entity = null;
			if (pid != null)
				entity = (URL)_redirectMap.get(pid);
			if (entity == null)
				entity = (URL)_redirectMap.get(sid);
			if (entity == null)
			{
				String dtd = sid;
				if (dtd.lastIndexOf('/') >= 0)
					dtd = dtd.substring(dtd.lastIndexOf('/') + 1);

				if (LOG.isDebugEnabled())
					LOG.debug("Can't exact match entity in redirect map, trying " + dtd);
				entity = (URL)_redirectMap.get(dtd);
			}

			if (entity != null)
			{
				try
				{
					InputStream in = entity.openStream();
					if (LOG.isDebugEnabled())
						LOG.debug("Redirected entity " + sid + " --> " + entity);
					InputSource is = new InputSource(in);
					is.setSystemId(sid);
					return is;
				} catch (IOException e)
				{
					LOG.ignore(e);
				}
			}
			return null;
		}
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	/**
	 * XML Attribute.
	 */
	public static class Attribute
	{

		private String _name;
		private String _value;

		Attribute(String n, String v)
		{
			_name = n;
			_value = v;
		}

		public String getName()
		{
			return _name;
		}

		public String getValue()
		{
			return _value;
		}
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	/**
	 * XML Node. Represents an XML element with optional attributes and ordered content.
	 */
	public static class Node extends AbstractList<Object>
	{

		Node _parent;
		private ArrayList<Object> _list;
		private String _tag;
		private Attribute[] _attrs;
		private boolean _lastString = false;
		private String _path;

		/* ------------------------------------------------------------ */
		Node(Node parent, String tag, Attributes attrs)
		{
			_parent = parent;
			_tag = tag;

			if (attrs != null)
			{
				_attrs = new Attribute[attrs.getLength()];
				for (int i = 0; i < attrs.getLength(); i++)
				{
					String name = attrs.getLocalName(i);
					if (name == null || name.equals(""))
						name = attrs.getQName(i);
					_attrs[i] = new Attribute(name, attrs.getValue(i));
				}
			}
		}

		/* ------------------------------------------------------------ */
		public Node getParent()
		{
			return _parent;
		}

		/* ------------------------------------------------------------ */
		public String getTag()
		{
			return _tag;
		}

		/* ------------------------------------------------------------ */
		public String getPath()
		{
			if (_path == null)
			{
				if (getParent() != null && getParent().getTag() != null)
					_path = getParent().getPath() + "/" + _tag;
				else
					_path = "/" + _tag;
			}
			return _path;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get an array of element attributes.
		 */
		public Attribute[] getAttributes()
		{
			return _attrs;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get an element attribute.
		 * 
		 * @return attribute or null.
		 */
		public String getAttribute(String name)
		{
			return getAttribute(name, null);
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get an element attribute.
		 * 
		 * @return attribute or null.
		 */
		public String getAttribute(String name, String dft)
		{
			if (_attrs == null || name == null)
				return dft;
			for (int i = 0; i < _attrs.length; i++)
				if (name.equals(_attrs[i].getName()))
					return _attrs[i].getValue();
			return dft;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get the number of children nodes.
		 */
		@Override
		public int size()
		{
			if (_list != null)
				return _list.size();
			return 0;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get the ith child node or content.
		 * 
		 * @return Node or String.
		 */
		@Override
		public Object get(int i)
		{
			if (_list != null)
				return _list.get(i);
			return null;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get the first child node with the tag.
		 * 
		 * @param tag
		 * @return Node or null.
		 */
		public Node get(String tag)
		{
			if (_list != null)
			{
				for (int i = 0; i < _list.size(); i++)
				{
					Object o = _list.get(i);
					if (o instanceof Node)
					{
						Node n = (Node)o;
						if (tag.equals(n._tag))
							return n;
					}
				}
			}
			return null;
		}

		/* ------------------------------------------------------------ */
		@Override
		public void add(int i, Object o)
		{
			if (_list == null)
				_list = new ArrayList<Object>();
			if (o instanceof String)
			{
				if (_lastString)
				{
					int last = _list.size() - 1;
					_list.set(last, (String)_list.get(last) + o);
				}
				else
					_list.add(i, o);
				_lastString = true;
			}
			else
			{
				_lastString = false;
				_list.add(i, o);
			}
		}

		/* ------------------------------------------------------------ */
		@Override
		public void clear()
		{
			if (_list != null)
				_list.clear();
			_list = null;
		}

		/* ------------------------------------------------------------ */
		/**
		 * Get a tag as a string.
		 * 
		 * @param tag The tag to get
		 * @param tags IF true, tags are included in the value.
		 * @param trim If true, trim the value.
		 * @return results of get(tag).toString(tags).
		 */
		public String getString(String tag, boolean tags, boolean trim)
		{
			Node node = get(tag);
			if (node == null)
				return null;
			String s = node.toString(tags);
			if (s != null && trim)
				s = s.trim();
			return s;
		}

		/* ------------------------------------------------------------ */
		@Override
		public synchronized String toString()
		{
			return toString(true);
		}

		/* ------------------------------------------------------------ */
		/**
		 * Convert to a string.
		 * 
		 * @param tag If false, only _content is shown.
		 */
		public synchronized String toString(boolean tag)
		{
			StringBuilder buf = new StringBuilder();
			toString(buf, tag);
			return buf.toString();
		}

		/* ------------------------------------------------------------ */
		/**
		 * Convert to a string.
		 * 
		 * @param tag If false, only _content is shown.
		 */
		public synchronized String toString(boolean tag, boolean trim)
		{
			String s = toString(tag);
			if (s != null && trim)
				s = s.trim();
			return s;
		}

		/* ------------------------------------------------------------ */
		private synchronized void toString(StringBuilder buf, boolean tag)
		{
			if (tag)
			{
				buf.append("<");
				buf.append(_tag);

				if (_attrs != null)
				{
					for (int i = 0; i < _attrs.length; i++)
					{
						buf.append(' ');
						buf.append(_attrs[i].getName());
						buf.append("=\"");
						buf.append(_attrs[i].getValue());
						buf.append("\"");
					}
				}
			}

			if (_list != null)
			{
				if (tag)
					buf.append(">");
				for (int i = 0; i < _list.size(); i++)
				{
					Object o = _list.get(i);
					if (o == null)
						continue;
					if (o instanceof Node)
						((Node)o).toString(buf, tag);
					else
						buf.append(o.toString());
				}
				if (tag)
				{
					buf.append("</");
					buf.append(_tag);
					buf.append(">");
				}
			}
			else if (tag)
				buf.append("/>");
		}

		/* ------------------------------------------------------------ */
		/**
		 * Iterator over named child nodes.
		 * 
		 * @param tag The tag of the nodes.
		 * @return Iterator over all child nodes with the specified tag.
		 */
		public Iterator<Node> iterator(final String tag)
		{
			return new Iterator<Node>()
			{

				int c = 0;
				Node _node;

				/* -------------------------------------------------- */
				public boolean hasNext()
				{
					if (_node != null)
						return true;
					while (_list != null && c < _list.size())
					{
						Object o = _list.get(c);
						if (o instanceof Node)
						{
							Node n = (Node)o;
							if (tag.equals(n._tag))
							{
								_node = n;
								return true;
							}
						}
						c++;
					}
					return false;
				}

				/* -------------------------------------------------- */
				public Node next()
				{
					try
					{
						if (hasNext())
							return _node;
						throw new NoSuchElementException();
					} finally
					{
						_node = null;
						c++;
					}
				}

				/* -------------------------------------------------- */
				public void remove()
				{
					throw new UnsupportedOperationException("Not supported");
				}
			};
		}
	}
}
